﻿//===================================================================================
// Copyright (c) Ryan Garrett.  All rights reserved.
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY
// OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT
// LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
// FITNESS FOR A PARTICULAR PURPOSE.
//===================================================================================

using Caffeine.Core.Metadata;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using System.Xml;

namespace Caffeine.Core
{
    /// <summary>
    /// Provides extension methods for the Caffeine.Core.ModelBase class
    /// </summary>
    public static class ModelBaseExtensions
    {
        #region Public

        /// <summary>
        /// Commits the entire model graph
        /// </summary>
        /// <param name="model">Root model</param>
        public static void CommitGraph(this ModelBase model)
        {
            model.Commit();

            foreach (ModelBase childModel in model.GetChildren(ModelStates.Committed))
            {
                childModel.CommitGraph();
            }
        }

        /// <summary>
        /// Resets the entire model graph to its previously committed state
        /// </summary>
        /// <param name="model">Root model</param>
        public static void ResetGraph(this ModelBase model)
        {
            model.Reset();

            foreach (ModelBase childModel in model.GetChildren(ModelStates.Committed))
            {
                childModel.ResetGraph();
            }
        }

        /// <summary>
        /// Validates the entire model graph
        /// </summary>
        /// <param name="model">Root model</param>
        public static void ValidateGraph(this ModelBase model)
        {
            model.Validate();

            foreach (ModelBase childModel in model.GetChildren(ModelStates.Added | ModelStates.Committed))
            {
                childModel.ValidateGraph();
            }
        }

        /// <summary>
        /// Gets the top most parent of this model
        /// </summary>
        /// <param name="model">Child model</param>
        /// <returns>Top most parent, or null if this model has no parent</returns>
        public static ModelBase GetRoot(this ModelBase model)
        {
            ModelBase parent = model.Parent;

            if (parent != null)
            {
                while (parent.Parent != null)
                {
                    parent = parent.Parent;
                }
            }

            return parent;
        }

        /// <summary>
        /// Searches the entire graph for an added or committed child that matches the name specified
        /// </summary>
        /// <param name="model">Root model</param>
        /// <param name="tagName">Name to search for</param>
        /// <returns>First child that matches the name specified, otherwise null</returns>
        public static ModelBase GetChild(this ModelBase model, String tagName)
        {
            ModelBase matchingChildModel = null;

            foreach (ModelBase childModel in model.GetChildrenInGraph())
            {
                if (childModel.TagName == tagName)
                {
                    matchingChildModel = childModel;
                    break;
                }
            }

            return matchingChildModel;
        }

        /// <summary>
        /// Gets all added or committed children immediately owned by this model
        /// </summary>
        /// <param name="model">Parent model</param>
        /// <returns>All direct added or committed children of this model</returns>
        public static IEnumerable<ModelBase> GetChildren(this ModelBase model)
        {
            return GetChildren(model, ModelStates.Added | ModelStates.Committed);
        }

        /// <summary>
        /// Gets all children immediately owned by this model which are in the state specified
        /// </summary>
        /// <param name="model">Parent model</param>
        /// <param name="modelState">State a child must be in to be returned</param>
        /// <returns>All direct children in the state specified</returns>
        public static IEnumerable<ModelBase> GetChildren(this ModelBase model, ModelStates modelState)
        {
            foreach (ModelPropertyBase property in model.ModelProperties)
            {
                if (property.IsModelProperty)
                {
                    ModelScalarPropertyBase modelProperty = property as ModelScalarPropertyBase;
                    ModelBase childModel = modelProperty.GetModelValue();

                    if (childModel != null)
                    {
                        if (childModel.IsAssignedToParentProperty && (modelState & ModelStates.Added) == ModelStates.Added)
                        {
                            yield return childModel;
                        }
                        else if (childModel.IsCommittedToParentProperty && (modelState & ModelStates.Committed) == ModelStates.Committed)
                        {
                            yield return childModel;
                        }
                        else
                        {
                            //Only removed models are to be returned
                        }
                    }

                    if ((modelState & ModelStates.Removed) == ModelStates.Removed && modelProperty.IsDirty)
                    {
                        childModel = modelProperty.GetCommittedModelValue();

                        if (childModel != null)
                        {
                            yield return childModel;
                        }
                    }
                }
                else if (property.IsModelCollectionProperty)
                {
                    ModelCollectionBase collectionProperty = property as ModelCollectionBase;

                    if 
                    (
                        (modelState & ModelStates.Added) == ModelStates.Added || 
                        (modelState & ModelStates.Committed) == ModelStates.Committed
                    )
                    {
                        foreach (ModelBase childModel in collectionProperty)
                        {
                            if (childModel.IsAddedToCollection && (modelState & ModelStates.Added) == ModelStates.Added)
                            {
                                yield return childModel;
                            }
                            else if (childModel.IsCommittedToCollection && (modelState & ModelStates.Committed) == ModelStates.Committed)
                            {
                                yield return childModel;
                            }
                            else
                            {
                                //Only removed models are to be retrieved. This else block should never execute.
                            }
                        }
                    }

                    if ((modelState & ModelStates.Removed) == ModelStates.Removed)
                    {
                        foreach (ModelBase childModel in collectionProperty.GetRemovedModels())
                        {
                            yield return childModel;
                        }
                    }
                }
                else
                {
                    //This is a value type or some other non-Model type property
                }
            }
        }

        /// <summary>
        /// Gets all added or committed children in this graph
        /// </summary>
        /// <param name="model">Root model</param>
        /// <returns>All added or committed children in this graph</returns>
        public static IEnumerable<ModelBase> GetChildrenInGraph(this ModelBase model)
        {
            return model.GetChildrenInGraph(ModelStates.Added | ModelStates.Committed, Int32.MaxValue);
        }

        /// <summary>
        /// Gets all children in this graph which are in the state specified
        /// </summary>
        /// <param name="model">Root parent</param>
        /// <param name="modelState">State a child must be in to be returned</param>
        /// <returns>All children in this graph which are in the state specified</returns>
        public static IEnumerable<ModelBase> GetChildrenInGraph(this ModelBase model, ModelStates modelState)
        {
            return model.GetChildrenInGraph(modelState, Int32.MaxValue);
        }

        /// <summary>
        /// Gets all added or committed children in this graph matching the name specified
        /// </summary>
        /// <param name="model">Root model</param>
        /// <param name="tagName">Name to search for</param>
        /// <returns>All added or committed children matching the name specified</returns>
        public static IEnumerable<ModelBase> GetChildrenInGraph(this ModelBase model, String tagName)
        {
            return model.GetChildrenInGraph(tagName, ModelStates.Added | ModelStates.Committed);
        }

        /// <summary>
        /// Gets all children in this graph matching the name and state specified
        /// </summary>
        /// <param name="model">Root model</param>
        /// <param name="tagName">Name to search for</param>
        /// <param name="modelState">State a child must be in to be returned</param>
        /// <returns>All children matching the name and state specified</returns>
        public static IEnumerable<ModelBase> GetChildrenInGraph(this ModelBase model, String tagName, ModelStates modelState)
        {
            return model.GetChildrenInGraph(tagName, modelState, Int32.MaxValue);
        }

        /// <summary>
        /// Gets all children in this graph matching the name and state specified up to a certain depth
        /// </summary>
        /// <param name="model">Root model</param>
        /// <param name="tagName">Name to search for</param>
        /// <param name="modelState">State a child must be in to be returned</param>
        /// <param name="maxDepth">Depth to stop searching. A depth of 0 will return only immediate children. A depth of Int32.MaxValue will search the entire graph.</param>
        /// <returns>All children matching the name and state specified within the depth allowed</returns>
        public static IEnumerable<ModelBase> GetChildrenInGraph(this ModelBase model, String tagName, ModelStates modelState, Int32 maxDepth)
        {
            foreach (ModelBase childModel in model.GetChildrenInGraph(modelState, maxDepth))
            {
                if (childModel.TagName == tagName)
                {
                    yield return childModel;
                }
            }
        }

        /// <summary>
        /// Gets all children in this graph matching the state specified and within the depth allowed
        /// </summary>
        /// <param name="model">Root model</param>
        /// <param name="modelState">State a child must be in to be returned</param>
        /// <param name="maxDepth">Depth to stop searching. A depth of 0 will return only immediate children. A depth of Int32.MaxValue will search the entire graph.</param>
        /// <returns>All children matching the state specified within the depth allowed</returns>
        public static IEnumerable<ModelBase> GetChildrenInGraph(this ModelBase model, ModelStates modelState, Int32 maxDepth)
        {
            return GetChildrenInGraphInternal(model, modelState, maxDepth, 0);
        }

        /// <summary>
        /// Release all observers of this graph. Any event handler registered with any model or property of this graph will be set to null. Use this to make sure
        /// this graph is not holding on to any visual objects that are observing it.
        /// </summary>
        /// <param name="model">Root model</param>
        public static void ReleaseObserversForGraph(this ModelBase model)
        {
            model.ReleaseObservers();

            foreach (ModelBase modelChild in model.GetChildren(ModelStates.Added | ModelStates.Committed | ModelStates.Removed))
            {
                modelChild.ReleaseObserversForGraph();
            }
        }

        /// <summary>
        /// Clears all model extensions from the graph
        /// </summary>
        /// <param name="model">Root model</param>
        public static void ClearExtensionsForGraph(this ModelBase model)
        {
            model.ClearModelExtension();

            foreach (ModelBase modelChild in model.GetChildren(ModelStates.Added | ModelStates.Committed | ModelStates.Removed))
            {
                modelChild.ClearExtensionsForGraph();
            }
        }

        /// <summary>
        /// Merge the values of one model into another
        /// </summary>
        /// <typeparam name="T">Target model type</typeparam>
        /// <typeparam name="S">Source model type</typeparam>
        /// <param name="targetModel">The target of the merge operation</param>
        /// <param name="sourceModel">The source of new values which will be applied to the target of the merge operation</param>
        public static void Merge<T, S>(this T targetModel, S sourceModel)
            where T : ModelBase, new()
            where S : T
        {
            if (sourceModel == null)
            {
                throw new ArgumentNullException("sourceModel");
            }
            
            if (typeof(T) != typeof(S))
            {
                throw new ArgumentException(String.Format("Type {0} is not the same type as {1}", typeof(T).FullName, typeof(S).FullName));
            }

            for (Int32 propertyIndex = 0; propertyIndex < targetModel.ModelProperties.Count; propertyIndex++)
            {
                targetModel.ModelProperties[propertyIndex].MergeAdjacentProperty(sourceModel.ModelProperties[propertyIndex]);
            }
        }

        #endregion

        #region Internal

        internal static Boolean CheckIsValidModelSubType(Type subType)
        {
            Boolean isValidModelBaseSubType = false;

            while (ModelBaseType != subType)
            {
                subType = subType.GetTypeInfo().BaseType;
                isValidModelBaseSubType = ModelBaseType == subType;

                if (isValidModelBaseSubType || subType == null)
                {
                    break;
                }
            }

            return isValidModelBaseSubType;
        }

        #endregion

        #region Private

        private static readonly Type ModelBaseType = typeof(ModelBase);

        private static IEnumerable<ModelBase> GetChildrenInGraphInternal(ModelBase model, ModelStates modelState, Int32 maxDepth, Int32 currentDepth)
        {
            foreach (ModelBase childModel in model.GetChildren(modelState | ModelStates.Committed))
            {
                if ((childModel.IsCommittedToCollection || childModel.IsCommittedToParentProperty) && currentDepth < maxDepth)
                {
                    //Only the paths of committed models are followed
                    foreach (ModelBase innerChildModel in GetChildrenInGraphInternal(childModel, modelState, maxDepth, currentDepth + 1))
                    {
                        yield return innerChildModel;
                    }
                }

                if 
                (
                    (modelState & ModelStates.Committed) == ModelStates.Committed && 
                    (childModel.IsCommittedToCollection || childModel.IsCommittedToParentProperty)
                )
                {
                    yield return childModel;
                }
                else if
                (
                    (modelState & ModelStates.Added) == ModelStates.Added &&
                    (childModel.IsAddedToCollection || childModel.IsAssignedToParentProperty)
                )
                {
                    yield return childModel;
                }
                else if
                (
                    (modelState & ModelStates.Removed) == ModelStates.Removed &&
                    (childModel.IsRemovedFromCollection || childModel.IsRemovedFromParentProperty)
                )
                {
                    yield return childModel;
                }
                else
                {
                    //The model did not contain the graph state that was specified by the modelState parameter
                }
            }
        }

        #endregion
    }
}
